fix(sticker): send WebP stickers as-is instead of re-encoding them - #166
fix(sticker): send WebP stickers as-is instead of re-encoding them#166FlavioPulli wants to merge 2 commits into
Conversation
`SendSticker` ran every sticker through `convertToWebP`, which fetches the URL, decodes with `image.Decode` and re-encodes with `webp.Encode(Quality: 80)`. That is wrong whenever the source is already a WebP — which is every sticker that came from WhatsApp itself: 1. Animated stickers cannot be sent at all. The registered decoder (chai2010/webp) only reads static WebP, so an animated file fails with `webpDecodeRGBA: failed` and the whole send dies. 2. The ones that do go through lose quality for nothing: a perfectly valid WebP is decoded and re-compressed at 80%. If the downloaded bytes are already a valid WebP they are now uploaded untouched, and `StickerMessage.IsAnimated` is set from the container flags — without it the recipient's client renders the first frame as a still image. Non-WebP input (PNG, JPEG) still goes through the conversion path, unchanged. How often this bites, measured on a real deployment: of 100 sticker files received by one instance, classified by the VP8X animation flag and by counting ANMF chunks, 61 were animated with 2+ frames and 4 carried the animation flag with a single frame — so 65 of 100 could not be re-sent. The single-frame ones are worth noting because they look perfectly still to the user, which makes the failure read as a bug in the product rather than a limitation. Two defensive details that the passthrough makes necessary: - The download is capped with an `io.LimitReader`. `http.Get` + `io.ReadAll` was unbounded, and now that the payload is uploaded rather than decoded, nothing downstream constrains its size either. - `isWebP` validates the declared RIFF size against the buffer length. The old conversion path rejected a truncated download for free (a truncated file fails to decode); a partial body still carries valid RIFF/WEBP magic and would be uploaded as-is, reaching the recipient broken. Verified end to end against a live instance: a sticker that previously failed with the error above was sent, delivered and read, with the animation intact.
Reviewer's GuideThis PR changes sticker sending so that WebP stickers are passed through without re-encoding, adds animation detection, and hardens sticker download and validation, while keeping non-WebP inputs on the existing conversion path. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- Consider replacing the bare
http.GetinstickerWebPwith a context-aware client (or passing through the existing request context) so sticker downloads respect timeouts and cancellations instead of potentially hanging indefinitely. - The error message
"failed to convert image to WebP"inSendStickeris now misleading when the input is already WebP and not re-encoded; updating it to something more generic like"failed to prepare sticker payload"would better reflect the new behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider replacing the bare `http.Get` in `stickerWebP` with a context-aware client (or passing through the existing request context) so sticker downloads respect timeouts and cancellations instead of potentially hanging indefinitely.
- The error message `"failed to convert image to WebP"` in `SendSticker` is now misleading when the input is already WebP and not re-encoded; updating it to something more generic like `"failed to prepare sticker payload"` would better reflect the new behavior.
## Individual Comments
### Comment 1
<location path="pkg/sendMessage/service/sticker_webp.go" line_range="38" />
<code_context>
+// A source that is already a valid WebP is returned untouched; anything else is decoded and
+// encoded to WebP as before.
+func stickerWebP(url string) ([]byte, error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ return nil, fmt.Errorf("failed to fetch image from URL: %v", err)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider using a context-aware HTTP client and checking the response status before reading the body.
This helper now does network I/O with `http.Get`, which uses the default client (no context, no timeout) and doesn’t validate the HTTP status. To avoid hangs and misinterpreting error pages as image data, please use a context-aware client (or one with a reasonable timeout, preferably passed in) and check `resp.StatusCode`, failing fast on non-2xx responses.
Suggested implementation:
```golang
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
// maxStickerBytes caps the sticker download. WhatsApp rejects stickers far smaller than this; the
// limit exists so a hostile URL cannot exhaust the process memory — relevant now that the payload
// is uploaded rather than decoded, so nothing downstream constrains its size either.
const maxStickerBytes = 10 << 20 // 10 MiB
// stickerWebP fetches the sticker URL and returns WebP bytes ready to upload.
//
// A source that is already a valid WebP is returned untouched; anything else is decoded and
// encoded to WebP as before.
func stickerWebP(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for image URL: %v", err)
}
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch image from URL: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return nil, fmt.Errorf("failed to fetch image from URL: unexpected HTTP status %s", resp.Status)
}
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxStickerBytes+1))
if err != nil {
return nil, fmt.Errorf("failed to read image from URL: %v", err)
}
if len(raw) > maxStickerBytes {
return nil, fmt.Errorf("sticker exceeds %d bytes", maxStickerBytes)
}
```
The edit above assumes this file either had no explicit import block or can be safely updated to include `context`, `fmt`, `io`, `net/http`, and `time` together. If an import block already exists elsewhere in this file, adjust the edit so that:
1. You only add `context` and `time` to the existing `import (...)` block rather than reintroducing `fmt`, `io`, and `net/http`.
2. You avoid duplicating the `import` keyword or any existing imports.
If your project uses a shared or injected `*http.Client`, you may want to replace the inline `client := &http.Client{...}` with that shared client while keeping the `NewRequestWithContext` and status-code check logic intact.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Two points from the review, both fair: - `http.Get` used the default client: no timeout, no context, and no status check. The sticker URL comes from the API caller and may point anywhere, so a server that accepts the connection and then stalls held the goroutine open indefinitely. Now it goes through a client with a 30s timeout via `http.NewRequestWithContext`, and a non-2xx response fails immediately — without that check an HTML error page was read as image data and failed later, deeper, with a decode error that said nothing about the URL having answered 404. - `failed to convert image to WebP` was describing something that no longer happens on the passthrough path, where nothing is converted. It is now `failed to prepare sticker payload`. The context is `context.Background()` at the call site, matching the adjacent `client.Upload` call. Threading the real request context through `SendSticker` would change the service interface, so I left it out of this PR — happy to do it if you would rather have it here.
|
Thanks @sourcery-ai — both points were fair, fixed in 7803311.
On the context: it is |
Problem
SendStickerruns every sticker throughconvertToWebP: fetch the URL, decode withimage.Decode, re-encode withwebp.Encode(Quality: 80). That is wrong whenever the source is already a WebP — which is every sticker that came from WhatsApp itself.How often it bites
Measured on a real deployment. We classified the 100 sticker files that had arrived in one instance, by the VP8X animation flag (
flags & 0x02) and by countingANMFchunks:65 of 100 received stickers could not be re-sent. Not an edge case — it is the majority of what a support agent can forward back to a customer.
The 4 single-frame ones deserve a mention, because they are the ones that make this look like a product bug rather than a limitation: the file sits in the animated container but does not move, so the user sees a perfectly still sticker being rejected as "animated".
Change
If the downloaded bytes are already a valid WebP, they are uploaded untouched — animation and quality survive. Non-WebP input (PNG, JPEG) still goes through the conversion path, unchanged.
StickerMessage.IsAnimatedis now set from the container flags; without it the recipient's client renders the first frame as a still image.Two defensive details that the passthrough makes necessary:
io.LimitReader.http.Get+io.ReadAllwas unbounded, and now that the payload is uploaded rather than decoded, nothing downstream constrains its size either.isWebPvalidates the declared RIFF size against the buffer length. The old conversion path rejected a truncated download for free (a truncated file fails to decode); a partial body still carries valid RIFF/WEBP magic and would be uploaded as-is, reaching the recipient broken.One new file plus two lines at the call site, to keep rebasing cheap.
Testing
Verified end to end against a live instance: a sticker that previously failed with the error above was sent, delivered and read, with the animation intact on the recipient's device.
go build ./...andgo vet ./pkg/sendMessage/...clean ondevelop.Relation to #151
@nicolasnovis got here first — #151 (2026-07-31) fixes the same bug the same way, and I only found it after writing this. It is based on
main, and on #128 @iagocotta asked that PRs targetdevelop, which is why this one exists separately rather than as a comment.I have no attachment to which one lands. If #151 is retargeted to
develop, I will close this and it can carry the fix; the two hardening bits above would then be worth folding in (they are the only substantive difference). Maintainers' call.Summary by Sourcery
Handle sticker sending by passing through existing WebP stickers unchanged, while still converting non-WebP images, and mark animated stickers correctly.
New Features:
Bug Fixes:
Enhancements: